<?php

namespace service\proxy;

use service\proxy\curl as http;
use service\proxy\redis as Redis;

/**
 * 代理列表维护类,基于redis
 * 每2分钟清空一次表,重新拉取,数量低于特定值,更新.
 * proxylist::get() 获取ip
 * proxylist::drop() 删除最后获取的ip,加参数则删除参数,参数可以是字符串或者数组
 * proxylist::moveToBlacklist() 删除最后获取的ip到公共坏IP池,参数类似drop()
 * proxylist::getInstance('class_name',[is_shared_instance]) 获取子类
 * proxylist::renew(param, 是否和坏ip池计算差积) 更新ip池子
 * 本类为抽象类,具体使用请该类的方法,请同命名空间下的子类,并自定义相关ip池名等参数.具体参见 ./kd100_proxy_pool.php
 */
abstract class proxylist
{
    /**
     * api_uri
     *
     * @var string
     */
    protected $api_uri = 'http://ent.kuaidaili.com/api/getproxy';

    /**
     * api_common_params
     *
     * @var array
     */
    protected $api_common_params = [
        'orderid' => '907159097084750',
        'num' => 200,
    ];

    /**
     * default api params
     *
     * @var array
     */
    protected $api_params = [
        'quality' => 2, //代理ip的稳定性 0: 不筛选(默认) 1: VIP稳定  2: SVIP企业版非常稳定
        // 'area' => '', //ip所在地区，支持按 国家/省/市 筛选  多个地区用英文逗号分隔，如 北京,上海
        // 'protocol' => '2', // 按代理协议筛选 1: HTTP, 2: HTTPS(同时也支持HTTP)
        //'method' => '1',  //按支持 GET/POST 筛选 1: 支持HTTP GET, 2: 支持 HTTP POST(同时也支持GET)
        // 'dedup' => 1, //  过滤今天提取过的IP  取值固定为1
        'an_ha' => 1, //返回的代理中只包含高匿名代理  取值固定为1
        'an_an' => 1, //返回的代理中只包含匿名代理   取值固定为1
        //'an_tr' => 1, //返回的代理中只包含透明代理   取值固定为1
        'sp1' => 1, //返回的代理中只包含极速代理（响应速度<1秒）  取值固定为1
        'sp2' => 1, //返回的代理中只包含快速代理（响应速度1~3秒） 取值固定为1
        //'sp3' => 1, //返回的代理中只包含慢速代理（响应速度>3秒）  取值固定为1
        // 'sort' => 1, // 返回的代理列表的排序方式    0: 默认排序  1: VIPSVIP企业版按响应速度(从快到慢)
        'format' => 'json', // 接口返回内容的格式   text: 文本格式(默认), json: VIPSVIP企业版json格式 xml : VIPSVIP企业版xml格式
        // 'sep' => '', // 提取结果列表中每个结果的分隔符 1: \r\n分隔(默认) 2: \n分隔  3: 空格分隔  4: |分隔
    ];

    /**
     * proxy update interval
     *
     * @var integer
     */
    protected $update_interval = 5;

    /**
     * last get ip
     *
     * @var string|null
     */
    protected $last_ip;

    /**
     * last time api returns
     *
     * @var array
     */
    protected $last_time_api_return;

    /**
     * update_on_destruct
     *
     * @var boolean
     */
    protected $update_on_destruct = true;

    /**
     * good ip store key
     *
     * @var string
     */
    const GOOD_IP = 'good_ips';

    /**
     * bad ip store key
     *
     * @var string
     */
    const BAD_IP = 'bad_ips';

    /**
     * common good ip store key
     *
     * @var string
     */
    const COMMON_GOOD_IP = 'common_good_ips';

    /**
     * common bad ip store key
     *
     * @var string
     */
    const COMMON_BAD_IP = 'common_bad_ips';

    /**
     * good ip update time
     *
     * @var string
     */
    const UPDATE_TIME = 'update_time';

    /**
     * minimal good ip number
     *
     * @var integer
     */
    const MIN_GOOD_IP_NUM = 50;

    /**
     * ip expires time
     *
     * @var integer
     */
    const EXPIRES_TIME = 120;

    /**
     * bad ip poll auto cleaning time
     *
     * @var integer
     */
    const AUTO_CLEANING_INTERVAL = 3600;

    /**
     * init api params
     *
     * @param array $_options api request uri params
     * @param integer $interval update interval
     * @return void
     */
    public function __construct(array $_options = [], $interval = null)
    {
        $this->setApiParams($_options);
        is_int($interval) and $this->update_interval = $interval;
    }

    /**
     * getInstance
     *
     * @param  string  $type
     * @param  boolean $new
     * @return service\proxy\proxylist
     */
    public static function getInstance($type = null, $new = false)
    {
        static $_instances = [];
        if(strpos($type, __NAMESPACE__) === false){
            $type = __NAMESPACE__.'\\'.$type;
        }
        if (!$new && array_key_exists($type, $_instances)) {
            return $_instances[$type];
        }
        if (class_exists($type)) {
            return $new ? new $type() : $_instances[$type] = new $type();
        }
        die('no such class exists');
    }

    /**
     * set api request params
     *
     * @param string|array $key
     * @param string $val
     * @return mixed
     */
    public function setApiParams($key, $val = null)
    {
        if (is_array($key)) {
            return $this->api_params = array_merge($this->api_params, $key);
        } elseif (is_string($key) && is_scalar($val)) {
            return $this->api_params[$key] = $val;
        }
        return false;
    }

    /**
     * getIpPoolLength
     *
     * @param string $type
     * @return integer
     */
    public function getIpPoolLength($type = null)
    {
        $type === null and $type = $this->getDefaultType() or is_bool($type) and $type = $this->getDefaultType($type);
        return Redis::sCard(static::keyBuilder($type));
    }

    /**
     * get a ip
     *
     * @return mixed
     */
    public function get()
    {
        if ($this->getIpPoolLength() <= static::MIN_GOOD_IP_NUM) {
            $this->updateIpPool([], true);
        } elseif (time() - (int) $this->getUpdateTime() > static::EXPIRES_TIME) {
            $this->clearIpPool();
            $this->renew();
        }
        return $this->last_ip = Redis::sRandMember(static::keyBuilder(static::GOOD_IP)) ?: false;
    }

    /**
     * add an ip to an ip pool
     *
     * @param string|array $ip
     * @param string $type  pool type
     * @todo add filter
     * @return integer
     */
    public function addIpToPool($ip = null, $type = null)
    {
        $type === null and $type = $this->getDefaultType() or is_bool($type) and $type = $this->getDefaultType($type);
        if (is_string($ip) && (bool) trim($ip)) {
            $ip = [$ip];
            // return Redis::sAdd(, $ip);
        }
        if (is_array($ip) && count($ip)) {
            return Redis::sAdd(static::keyBuilder($type), ...$ip);
        }
    }

    /**
     * getLastGetIp
     *
     * @todo store in redis
     * @return string|null
     */
    public function getLastGetIp()
    {
        return $this->last_ip;
    }

    /**
     * getLastTimeApiReturn
     *
     * @todo store in redis
     * @return string|null
     */
    public function getLastTimeApiReturn()
    {
        return $this->last_time_api_return;
    }

    /**
     * get all ips
     *
     * @param string $type ip pool type
     * @return array|null
     */
    public function getAll($type = null)
    {
        $type === null and $type = $this->getDefaultType() or is_bool($type) and $type = $this->getDefaultType($type);
        return Redis::sMembers(static::keyBuilder($type));
    }

    /**
     * mannul update ip list
     *
     * @param array $params
     * @param boolean $diff_badip
     * @return boolean
     */
    public function renew(array $params = [], $diff_badip = false)
    {
        return $this->updateIpPool(array_merge($this->api_params, (array) $params), true, $diff_badip);
    }

    /**
     * drop some ips from good ip list
     *
     * @param  string|array $ips
     * @param  string $src_pool
     * @param  string $desc_pool
     * @return array|null
     */
    public function drop($ips = null, $src_pool = null, $desc_pool = null)
    {
        if (is_string($ips)) {
            $ips = [$ips];
        } elseif ($ips === null) {
            $ips = [$this->getLastGetIp()];
        }
        if (is_array($ips)) {
            if (is_string($src_pool)) {
                $_store_key = static::keyBuilder($src_pool);
                foreach ($ips as $k => $val) {
                    $ips[$k] = Redis::sRem($_store_key, $val);
                }
                return $ips;
            }
            $_bad_ip = static::keyBuilder(static::BAD_IP);
            $_good_ip = static::keyBuilder(static::GOOD_IP);
            if (is_string($src_pool) && is_string($desc_pool)) {
                $_bad_ip = static::keyBuilder($desc_pool);
                $_good_ip = static::keyBuilder($src_pool);
            }
            foreach ($ips as $k => $val) {
                $ips[$k] = Redis::sMove($_good_ip, $_bad_ip, $val);
            }
            return $ips;
        }
    }

    /**
     * block some ips
     *
     * @param  string|array $ips
     * @return array|null
     */
    public function moveToBlacklist($ips = null)
    {
        if (is_string($ips)) {
            $ips = [$ips];
        } elseif ($ips === null) {
            $ips = [$this->getLastGetIp()];
        }
        if (is_array($ips)) {
            $_bad_ip = static::keyBuilder(static::COMMON_BAD_IP);
            $_good_ip = static::keyBuilder(static::GOOD_IP);
            foreach ($ips as $k => $val) {
                $ips[$k] = Redis::sMove($_good_ip, $_bad_ip, $val);
            }
            return $ips;
        }
    }

    /**
     * clear an ip pool
     *
     * @param string $key
     * @return boolean
     */
    public function clearIpPool($key = null)
    {
        $key === null and $key = static::GOOD_IP or is_bool($key) and $key = $this->getDefaultType($key);
        return Redis::del(static::keyBuilder($key));
    }

    /**
     * get good and bad ip Diff
     *
     * @param  string $key_a
     * @param  string $key_b
     * @return array
     */
    public function getIpPoolDiff($key_a = null, $key_b = null)
    {
        $key_a === null and $key_a = $this->getDefaultType(); //static::GOOD_IP
        $key_b === null and $key_b = $this->getDefaultType(false); //static::BAD_IP
        return Redis::sDiff(static::keyBuilder($key_a), static::keyBuilder($key_b));
    }

    /**
     * setExpire
     *
     * @param string|boolean|null $key  default is common bad ip, false is bad ip, true is good ip
     * @param integer $time
     * @param boolean $pexpire
     * @return boolean
     */
    public function setExpire($key = null, $time = 600, $pexpire = false)
    {
        $key === null and $key = static::COMMON_BAD_IP or is_bool($key) and $key = $this->getDefaultType($key);
        return $pexpire ? Redis::pExpire(self::keyBuilder($key), $time) : Redis::expire(self::keyBuilder($key), (int) $time ?: 600);
    }

    /**
     * return a key's ttl
     *
     * @param  string|boolean|null $key default is common bad ip, false is bad ip, true is good ip
     * @param  boolean $pttl
     * @return integer
     */
    public function ttl($key, $pttl = false)
    {
        $key === null and $key = static::COMMON_BAD_IP or is_bool($key) and $key = $this->getDefaultType($key);
        return $pttl ? Redis::pttl(self::keyBuilder($key)) : Redis::ttl(self::keyBuilder($key));
    }

    /**
     * updateOnDestruct
     *
     * @return boolean
     */
    public function updateOnDestruct($status = null)
    {
        return $status === null ? $this->update_on_destruct : $this->update_on_destruct = (bool) $status;
    }

    /**
     * update ip
     *
     * @param array $params
     * @param  boolean $force
     * @param  boolean $diff_badip store unintersect with bad ip
     * @return boolean|int
     */
    protected function updateIpPool(array $params = [], $force = false, $diff_badip = true)
    {
        if ($force || time() - (int) $this->getUpdateTime() > $this->update_interval) {
            $url = $this->buildRequestUri(array_merge($this->api_params, (array) $params));
            if ($this->last_time_api_return = $data = http::get($url)) {
                if ($data = json_decode($data, true) and isset($data['data']['count']) && $data['data']['count'] > 0) {
                    $this->setUpdateTime();
                    //$added_count = call_user_func_array(['\service\proxy\redis', 'sAdd'], array_merge([static::keyBuilder(static::GOOD_IP)], $data['data']['proxy_list']));
                    $added_count = Redis::sAdd(static::keyBuilder(static::GOOD_IP), ...$data['data']['proxy_list']);
                    mt_rand(0, 4) === 2 and Redis::sDiffStore(static::keyBuilder(static::GOOD_IP), static::keyBuilder(static::GOOD_IP), static::keyBuilder(static::COMMON_BAD_IP));
                    if ($diff_badip === true) {
                        return Redis::sDiffStore(static::keyBuilder(static::GOOD_IP), static::keyBuilder(static::GOOD_IP), static::keyBuilder(static::BAD_IP));
                    }
                    return $added_count;
                } else {
                    //@todo write log
                    //alerm
                }
            } else {
                //request failed
                //alerm
            }
        }
        return false;
    }

    /**
     * buildRequestUri
     *
     * @param array $params custom parameters for api call
     * @return string
     */
    protected function buildRequestUri(array $params = [])
    {
        return $this->api_uri . '?' . http_build_query(array_merge($this->api_common_params, $params));
    }

    /**
     * getUpdateTime
     *
     * @return integer
     */
    protected function getUpdateTime()
    {
        return Redis::get(static::keyBuilder(static::UPDATE_TIME));
    }

    /**
     * setUpdateTime
     *
     * @return void
     */
    protected function setUpdateTime()
    {
        return Redis::set(static::keyBuilder(static::UPDATE_TIME), microtime(true), 86400);
    }

    /**
     * getDefaultType
     *
     * @param string $good
     * @return string
     */
    protected function getDefaultType($good = true)
    {
        return $good ? static::GOOD_IP : static::BAD_IP;
    }

    /**
     * redis store keyBuilder
     *
     * @param string $key
     * @return string
     */
    protected static function keyBuilder($key = null)
    {
        if (!is_string($key)) {
            die('参数配置错误');
        }
        return '__express_proxy_list__' . $key;
    }

    /**
     * autoCleaning ip pool
     *
     * @param string|null $key
     * @return void
     */
    protected function autoClean($key = null)
    {
        $key === null and $key = __CLASS__ . '_autoClean_timestamp';
        $now = microtime(true);

        if ($now - (float) Redis::get($key) > static::AUTO_CLEANING_INTERVAL) {
            $this->clearIpPool(static::COMMON_BAD_IP);
            $this->clearIpPool(static::BAD_IP);
            Redis::set(self::keyBuilder($key), $now);
        }
    }

    /**
     * on destruct
     */
    public function __destruct()
    {
        // $this->autoClean();
        $this->updateOnDestruct() and $this->updateIpPool();
    }
}
